/**
 * Copyright (c) 2007-2008 Nearchos Paspallis
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */

/**
 * AspectSense
 *
 * Middleware for annotation-based, context-aware and adaptive services for
 * mobile and ubiquitous computing
 *
 * This project is an open-source effort, initiated by Nearchos Paspallis as
 * part of his PhD work at the University of Cyprus (http://www.ucy.ac.cy).
 *
 * More information about the project is available at "http://aspectsense.org".
 */

package org.aspectsense.gui.client;

import org.jgraph.graph.*;
import org.jgraph.JGraph;
import org.aspectsense.framework.MonitoringManagement;
import org.aspectsense.framework.RequiredServiceMetadata;
import org.aspectsense.metadata.ComponentMetadata;

import javax.swing.*;
import java.awt.*;
import java.awt.event.MouseEvent;
import java.awt.geom.Rectangle2D;
import java.rmi.RemoteException;
import java.util.*;

/**
 * @author Nearchos Paspallis [nearchos@cs.ucy.ac.cy]
 *         Date: Feb 18, 2008
 *         Time: 8:31:43 AM
 */
public class ApplicationGraph extends JGraph
{
    public static final int SERVICE_WIDTH = 150;
    public static final int SERVICE_HEIGHT = 30;
    public static final int COMPONENT_WIDTH = 100;
    public static final int COMPONENT_HEIGHT = 30;
    public static final int HORIZONTAL_GAP = 50;
    public static final int VERTICAL_GAP = 100;
    public static final int HORIZONTAL_MARGIN = 10;
    public static final int VERTICAL_MARGIN = 30;
    public static final int DISTANCE_BETWEEN_COMPONENTS_AND_SERVICES = 200;

    public static final Color DARK_RED = new Color(128, 0, 0);
    public static final Color DARK_BLUE = new Color(0, 0, 128);

    public ApplicationGraph()
    {
        super(new DefaultGraphModel());
    }

    private final Map<DefaultGraphCell, String> tooltips
            = new HashMap<DefaultGraphCell, String>();

    // Return Cell Label as a Tooltip
    public String getToolTipText(final MouseEvent e)
    {
        if(e != null)
        {
            // Fetch Cell under Mouse-pointer
            final Object cell = getFirstCellForLocation(e.getX(), e.getY());
            if(cell != null && cell instanceof DefaultGraphCell)
            {
                // Convert Cell to String and Return
                final DefaultGraphCell defaultGraphCell = (DefaultGraphCell) cell;
                return tooltips.get(defaultGraphCell);
            }
      }
      return null;
    }

    private final Set<DefaultGraphCell> previousCells = new HashSet<DefaultGraphCell>();

    public void updateModel(final MonitoringManagement monitoringManagement)
            throws RemoteException
    {
        if (monitoringManagement == null)
        {
            throw new NullPointerException("Illegal null argument");
        }

        final String [] installedComponents = monitoringManagement.getInstalledComponentIDs();
        Arrays.sort(installedComponents);
        final String [] installedServices = monitoringManagement.getInstalledServiceIDs();
        Arrays.sort(installedServices);

        final int numberOfComponents = installedComponents.length;
        final int numberOfServices = installedServices.length;

        final int servicesWidth = numberOfServices * SERVICE_WIDTH
                + (numberOfServices - 1) * HORIZONTAL_GAP
                + 2 * HORIZONTAL_MARGIN;
        final int componentsWidth = numberOfComponents * COMPONENT_WIDTH
                + (numberOfComponents - 1) * HORIZONTAL_GAP
                + 2 * HORIZONTAL_MARGIN;
        final int maxWidth = Math.max(servicesWidth, componentsWidth);

        final Map<String, DefaultGraphCell> serviceToCellMap = new HashMap<String, DefaultGraphCell>();
        // create service nodes
        {
            int leftOffset = HORIZONTAL_MARGIN;
            if(servicesWidth < maxWidth)
            {
                leftOffset = leftOffset + (maxWidth - servicesWidth) / 2;
            }
            int x = leftOffset, y = (int) (VERTICAL_MARGIN + 0.5 * SERVICE_HEIGHT);
            for(final String serviceID : installedServices)
            {
                final DefaultGraphCell serviceGraphCell = createServiceNode(serviceID, x, y);
                serviceToCellMap.put(serviceID, serviceGraphCell);
                x = x + (SERVICE_WIDTH + HORIZONTAL_GAP);
            }
        }

        final Map<String, DefaultGraphCell> componentToCellMap = new HashMap<String, DefaultGraphCell>();
        // create component nodes
        {
            int leftOffset = HORIZONTAL_MARGIN;
            if(componentsWidth < maxWidth)
            {
                leftOffset = leftOffset + (maxWidth - componentsWidth) / 2;
            }
            int x = leftOffset, y = (int) (DISTANCE_BETWEEN_COMPONENTS_AND_SERVICES + VERTICAL_MARGIN + 0.5 * COMPONENT_HEIGHT);
            for(final String componentID : installedComponents)
            {
                final ComponentMetadata componentMetadata = monitoringManagement.getComponentMetadata(componentID);
                final DefaultGraphCell componentGraphCell = createComponentNode(componentMetadata, x, y);
                componentToCellMap.put(componentID, componentGraphCell);
                x = x + (COMPONENT_WIDTH + HORIZONTAL_GAP);
            }
        }

        final Set<DefaultEdge> providedServiceEdgeCells = new HashSet<DefaultEdge>();
        final Set<DefaultEdge> requiredServicesEdgeCells = new HashSet<DefaultEdge>();
        // create edges
        {
            for(final String componentID : installedComponents)
            {
                final ComponentMetadata componentMetadata = monitoringManagement.getComponentMetadata(componentID);
                final String providedServiceID = componentMetadata.getProvidedService().getServiceTypeCanonicalName();

                {
                    final DefaultGraphCell sourceComponent = componentToCellMap.get(componentID);
                    final DefaultGraphCell targetService = serviceToCellMap.get(providedServiceID);

                    final DefaultEdge providedServiceEdgeCell = createEdge(sourceComponent, targetService, DARK_BLUE);
                    providedServiceEdgeCells.add(providedServiceEdgeCell);
                }

                final Map<String, RequiredServiceMetadata> requiredServices = componentMetadata.getRequiredServices();
                final Set<String> requiredServiceIDs = requiredServices.keySet();
                for(final String requiredServiceID : requiredServiceIDs)
                {
                    final DefaultGraphCell sourceService = serviceToCellMap.get(requiredServiceID);
                    final DefaultGraphCell targetComponent = componentToCellMap.get(componentID);
                    final DefaultEdge requiredServiceEdgeCell = createEdge(sourceService, targetComponent, DARK_RED);
                    requiredServicesEdgeCells.add(requiredServiceEdgeCell);
                }
            }
        }

        super.graphLayoutCache.insert(componentToCellMap.values().toArray());
        super.graphLayoutCache.insert(serviceToCellMap.values().toArray());
        super.graphLayoutCache.insert(providedServiceEdgeCells.toArray());
        super.graphLayoutCache.insert(requiredServicesEdgeCells.toArray());

        super.graphLayoutCache.remove(previousCells.toArray());

        previousCells.clear();
        previousCells.addAll(componentToCellMap.values());
        previousCells.addAll(serviceToCellMap.values());
        previousCells.addAll(providedServiceEdgeCells);
        previousCells.addAll(requiredServicesEdgeCells);
    }

    private DefaultGraphCell createComponentNode(
            final ComponentMetadata componentMetadata,
            final double x,
            final double y)
    {
        // Create vertex with the given name
        final DefaultGraphCell cell = new DefaultGraphCell(componentMetadata.getDisplayID());

        tooltips.put(cell, createComponentTooltip(componentMetadata));

        // Set bounds
        GraphConstants.setBounds(cell.getAttributes(),
                new Rectangle2D.Double(x, y, COMPONENT_WIDTH, COMPONENT_HEIGHT));

        // Set fill color
//        GraphConstants.setGradientColor(cell.getAttributes(), DARK_BLUE);
        GraphConstants.setBackground(cell.getAttributes(), Color.BLUE);
        GraphConstants.setForeground(cell.getAttributes(), Color.YELLOW);
        GraphConstants.setOpaque(cell.getAttributes(), true);

        GraphConstants.setBorder(cell.getAttributes(), BorderFactory.createRaisedBevelBorder());

        // Add a Port
        DefaultPort port = new DefaultPort();
        cell.add(port);
        port.setParent(cell);

        return cell;
    }

    private DefaultGraphCell createServiceNode(
            final String serviceID,
            final double x,
            final double y)
    {
        // Create vertex with the given name
        final String displayID = serviceID.length() > 24 ?
                "..." + serviceID.substring(serviceID.length() - 21)
                :
                serviceID;
        final DefaultGraphCell cell = new DefaultGraphCell(displayID);

        tooltips.put(cell, serviceID);

        // Set bounds
        GraphConstants.setBounds(cell.getAttributes(),
                new Rectangle2D.Double(x, y, SERVICE_WIDTH, SERVICE_HEIGHT));

        // Set fill color
//        GraphConstants.setGradientColor(cell.getAttributes(), DARK_RED);
        GraphConstants.setBackground(cell.getAttributes(), Color.RED);
        GraphConstants.setOpaque(cell.getAttributes(), true);

        GraphConstants.setBorder(cell.getAttributes(), BorderFactory.createRaisedBevelBorder());

        // Add a Port
        DefaultPort port = new DefaultPort();
        cell.add(port);
        port.setParent(cell);

        return cell;
    }

    private DefaultEdge createEdge(
            final DefaultGraphCell source,
            final DefaultGraphCell target,
            final Color color)
    {
        final DefaultEdge edge = new DefaultEdge();
        // Fetch the ports from the new vertices, and connect them with the edge
        edge.setSource(source.getChildAt(0));
        edge.setTarget(target.getChildAt(0));

        // Set Arrow Style for edge
        final int arrow = GraphConstants.ARROW_CLASSIC;
        GraphConstants.setLineEnd(edge.getAttributes(), arrow);
        GraphConstants.setEndFill(edge.getAttributes(), true);
        GraphConstants.setLineColor(edge.getAttributes(), color);

        return edge;
    }

    private String createComponentTooltip(final ComponentMetadata componentMetadata)
    {
        final StringBuilder tooltipStringBuilder = new StringBuilder();

        tooltipStringBuilder.append("<html><b>")
                .append(componentMetadata.getComponentTypeCanonicalName())
                .append("</b><br/>Provided service: ")
                .append(componentMetadata.getProvidedService());
        final Map<String, RequiredServiceMetadata> requiredServices
                = componentMetadata.getRequiredServices();

        if(requiredServices.isEmpty())
        {
            tooltipStringBuilder.append("<br/>No required services");
        }
        else
        {
            tooltipStringBuilder.append("<br/>Required services: <br/>");
            final Set<String> requiredServiceIDs = requiredServices.keySet();
            for(final String requiredServiceID : requiredServiceIDs)
            {
                final RequiredServiceMetadata requiredServiceMetadata
                        = requiredServices.get(requiredServiceID);
                final String variationPointID
                        = requiredServiceMetadata.getVariationPointID();
                final String requiredServiceCanonicalName
                        = requiredServiceMetadata.getServiceTypeCanonicalName();
                tooltipStringBuilder.append("&nbsp;- <b>").append(variationPointID)
                        .append("</b> of type ")
                        .append(requiredServiceCanonicalName).append("<br/>");
            }
            tooltipStringBuilder.append("<br/>");
        }
        tooltipStringBuilder.append("</html>");

        return tooltipStringBuilder.toString();
    }
}